package net.vtstar.vtcloud.framework.system.api.util;

import com.google.common.base.Ascii;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Enumeration;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.function.Predicate;
import java.util.regex.Pattern;

/**
 * com.linecorp.armeria.common.util.SystemInfo
 */
public final class SystemInfo {

    private static final Logger logger = LoggerFactory.getLogger(SystemInfo.class);
    private static final int JAVA_VERSION;
    private static final OsType osType;
    
    static {
        int javaVersion = -1;
        try {
            final String spec = System.getProperty("java.specification.version");
            if (spec != null) {
                final String[] strValues = spec.split("\\.");
                final int major;
                final int minor;

                switch (strValues.length) {
                    case 0:
                        major = 0;
                        minor = 0;
                        break;
                    case 1:
                        major = Integer.parseInt(strValues[0]);
                        minor = 0;
                        break;
                    default:
                        major = Integer.parseInt(strValues[0]);
                        minor = Integer.parseInt(strValues[1]);
                }

                if (major > 1) {
                    javaVersion = major;
                } else if (major == 1) {
                    if (minor == 0) {
                        javaVersion = 1;
                    } else if (minor > 0) {
                        javaVersion = minor;
                    }
                }
            }

            if (javaVersion > 0) {
                logger.debug("Java version: {}", javaVersion);
            } else {
                logger.warn("'java.specification.version' contains an unexpected value: {}", spec);
            }
        } catch (Throwable t) {
            logger.warn("Failed to determine Java version", t);
        }

        JAVA_VERSION = javaVersion > 0 ? javaVersion : 8;

        final String osName = Ascii.toUpperCase(System.getProperty("os.name", ""));
        if (osName.startsWith("WINDOWS")) {
            osType = OsType.WINDOWS;
        } else if (osName.startsWith("LINUX")) {
            osType = OsType.LINUX;
        } else if (osName.startsWith("MAC")) {
            osType = OsType.MAC;
        } else {
            osType = OsType.OTHERS;
        }
    }

    private SystemInfo() {}

    public static <T> T firstNonNull(T first, T second) {
        if (first != null) {
            return first;
        }
        if (second != null) {
            return second;
        }
        throw new NullPointerException("Both parameters are null");
    }

    public static Throwable peel(Throwable throwable) {
        Objects.requireNonNull(throwable, "throwable");
        Throwable cause = throwable.getCause();
        while (cause != null && cause != throwable &&
                (throwable instanceof CompletionException || throwable instanceof ExecutionException ||
                        throwable instanceof InvocationTargetException ||
                        throwable instanceof ExceptionInInitializerError)) {
            throwable = cause;
            cause = throwable.getCause();
        }
        return throwable;
    }

    public static int javaVersion() {
        return JAVA_VERSION;
    }

    public static boolean isLinux() {
        return osType == OsType.LINUX;
    }

    /**
     * Returns the operating system for the currently running process.
     */
    public static OsType osType() {
        return osType;
    }

    public static String hostname() {
        return Hostname.HOSTNAME;
    }

    public static int pid() {
        if (Pid.PID <= 0) {
            throw new IllegalStateException("Failed to retrieve the current PID.");
        }
        return Pid.PID;
    }

    public static Inet4Address defaultNonLoopbackIpV4Address() {
        return DefaultNonLoopbackIPv4Address.defaultNonLoopbackIpV4Address;
    }

    public static boolean hasIpV6() {
        return HasIpV6.HAS_IPV6;
    }

    private static final class Hostname {
        private static final Pattern HOSTNAME_PATTERN = Pattern.compile(
                "^(?:[-_a-zA-Z0-9]|[-_a-zA-Z0-9][-_.a-zA-Z0-9]*[-_a-zA-Z0-9])$");
        static final String HOSTNAME;
        
        static {
            // Try /proc/sys/kernel/hostname on Linux.
            String hostname = null;
            if (isLinux()) {
                try {
                    final List<String> lines = Files.readAllLines(Paths.get("/proc/sys/kernel/hostname"));
                    if (!lines.isEmpty()) {
                        hostname = normalizeHostname(lines.get(0));
                    }
                    if (hostname != null) {
                        logger.info("hostname: {} (from /proc/sys/kernel/hostname)", hostname);
                    } else {
                        logger.debug("/proc/sys/kernel/hostname does not contain a valid hostname: {}", lines);
                    }
                } catch (Throwable t) {
                    logger.debug("Failed to get the hostname from /proc/sys/kernel/hostname; " +
                            "using the 'hostname' command instead", t);
                }
            }

            // Try /usr/bin/hostname.
            if (hostname == null) {
                Process process = null;
                try {
                    process = Runtime.getRuntime().exec("hostname");
                    final BufferedReader in = new BufferedReader(
                            new InputStreamReader(process.getInputStream()));
                    final String line = in.readLine();
                    if (line != null) {
                        hostname = normalizeHostname(line);
                    }

                    if (hostname != null) {
                        logger.info("hostname: {} (from 'hostname' command)", hostname);
                    } else {
                        logger.debug("The 'hostname' command returned a non-hostname ({}); " +
                                "using InetAddress.getLocalHost() instead", line);
                    }
                } catch (Throwable t) {
                    logger.debug("Failed to get the hostname using the 'hostname' command; " +
                            "using InetAddress.getLocalHost() instead", t);
                } finally {
                    if (process != null) {
                        process.destroy();
                    }
                }
            }

            if (hostname == null) {
                try {
                    final String jdkHostname = InetAddress.getLocalHost().getHostName();
                    hostname = normalizeHostname(jdkHostname);
                    if (hostname == null) {
                        logger.warn("InetAddress.getLocalHost() returned an invalid hostname ({}); " +
                                "using 'localhost' instead", jdkHostname);
                    } else {
                        logger.info("hostname: {} (from InetAddress.getLocalHost())", hostname);
                    }
                } catch (Throwable t) {
                    logger.warn("Failed to get the hostname using InetAddress.getLocalHost(); " +
                            "using 'localhost' instead", t);
                }
            }

            HOSTNAME = firstNonNull(hostname, "localhost");
        }

        private static String normalizeHostname(String line) {
            final String hostname = IDN.toASCII(line.trim(), IDN.ALLOW_UNASSIGNED);
            if (!HOSTNAME_PATTERN.matcher(hostname).matches()) {
                return null;
            }
            return hostname.toLowerCase();
        }
    }

    private static final class Pid {
        static final int PID;

        static {
            int pid = -1;
            // Try ProcessHandle.pid() if Java 9+.
            if (javaVersion() >= 9) {
                try {
                    final Class<?> handleClass = Class.forName("java.lang.ProcessHandle", true,
                            Process.class.getClassLoader());
                    final Method currentMethod = handleClass.getDeclaredMethod("current");
                    final Method pidMethod = handleClass.getDeclaredMethod("pid");
                    final Object currentHandle = currentMethod.invoke(null);
                    final Object result = pidMethod.invoke(currentHandle);
                    pid = validatePid(result);
                    if (pid <= 0) {
                        logger.warn("ProcessHandle.pid() returned an invalid PID: {}", result);
                    } else {
                        logger.info("PID: {} (from ProcessHandle.pid())", pid);
                    }
                } catch (Throwable t) {
                    logFailure("ProcessHandle.current()", true, t);
                }
            }

            // Try sun.management.VMManagement.getProcessId().
            if (pid <= 0) {
                try {
                    final RuntimeMXBean runtime = ManagementFactory.getRuntimeMXBean();
                    final Field jvmField = runtime.getClass().getDeclaredField("jvm");
                    jvmField.setAccessible(true);
                    final Object jvm = jvmField.get(runtime);
                    final Method getProcessIdMethod = jvm.getClass().getDeclaredMethod("getProcessId");
                    getProcessIdMethod.setAccessible(true);
                    final Object result = getProcessIdMethod.invoke(jvm);
                    pid = validatePid(result);
                    if (pid <= 0) {
                        logger.warn("VMManagement.getProcessId() returned an invalid PID: {}", result);
                    } else {
                        logger.info("PID: {} (from VMManagement.getProcessId())", pid);
                    }
                } catch (Throwable t) {
                    logFailure("VMManagement.getProcessId()", false, t);
                }
            }

            // Try /proc/self (Linux only)
            if (pid <= 0 && isLinux()) {
                try {
                    final Path path = Paths.get("/proc/self");
                    if (Files.isSymbolicLink(path)) {
                        final Path realPath = path.toRealPath();
                        pid = validatePid(Integer.parseInt(realPath.getFileName().toString()));
                        if (pid <= 0) {
                            logger.warn("/proc/self does not refer to a PID-named file: {}", realPath);
                        } else {
                            logger.info("PID: {} (from /proc/self)", pid);
                        }
                    }
                } catch (Throwable t) {
                    logFailure("/proc/self", false, t);
                }
            }

            // Try RuntimeMXBean.getName() as the last resort.
            if (pid <= 0) {
                try {
                    final String result = ManagementFactory.getRuntimeMXBean().getName();
                    final String[] values = result.split("@");
                    pid = validatePid(values.length > 0 ? Integer.parseInt(values[0]) : -1);
                    if (pid <= 0) {
                        logger.warn("RuntimeMXBean.getName() returned an unexpected value: {}", result);
                    } else {
                        logger.info("PID: {} (from RuntimeMXBean.getName())", pid);
                    }
                } catch (Throwable t) {
                    logFailure("RuntimeMXBean.getName()", true, t);
                }
            }

            PID = pid;
        }

        private static int validatePid(Object value) {
            if (!(value instanceof Number)) {
                return -1;
            } else {
                final int pid = ((Number) value).intValue();
                return pid > 0 ? pid : -1;
            }
        }

        private static void logFailure(String method, boolean warn, Throwable cause) {
            cause = peel(cause);
            if (cause instanceof UnsupportedOperationException ||
                    cause instanceof SecurityException ||
                    cause instanceof IllegalAccessException) {
                logger.debug("An access to {} not possible due to platform restriction:", method, cause);
                return;
            }

            final String msg = "Failed to retrieve the current PID from {}:";
            if (warn) {
                logger.warn(msg, method, cause);
            } else {
                logger.debug(msg, method, cause);
            }
        }
    }

    private static final class DefaultNonLoopbackIPv4Address {

        // Forked from InetUtils in spring-cloud-common 3.0.0.M1 at e7bb7ed3ae19a91c6fa7b3b698dd9788f70df7d4
        // - Use CIDR in isPreferredAddress instead of regular expression.

        static final Inet4Address defaultNonLoopbackIpV4Address;

        static {
            Inet4Address result = null;
            String nicDisplayName = null;
            try {
                int lowest = Integer.MAX_VALUE;
                for (final Enumeration<NetworkInterface> nics = NetworkInterface.getNetworkInterfaces();
                     nics.hasMoreElements();) {
                    final NetworkInterface nic = nics.nextElement();
                    if (!nic.isUp()) {
                        logger.debug("{} is down. Trying next.", nic.getDisplayName());
                        continue;
                    }

                    // The NIC whose index is the lowest will be likely the valid IPv4 address.
                    // See https://github.com/spring-cloud/spring-cloud-commons/issues/82.
                    if (nic.getIndex() < lowest || result == null) {
                        lowest = nic.getIndex();
                    } else {
                        logger.debug("{} has higher index({}) than {}. Skip.",
                                nic.getDisplayName(), nic.getIndex(), result);
                        continue;
                    }

                    for (final Enumeration<InetAddress> addrs = nic.getInetAddresses();
                         addrs.hasMoreElements();) {
                        final InetAddress address = addrs.nextElement();
                        if (!(address instanceof Inet4Address)) {
                            logger.debug("{} of {} is not an Inet4Address. Trying next.",
                                    address, nic.getDisplayName());
                            continue;
                        }
                        if (address.isLoopbackAddress()) {
                            logger.debug("{} of {} is a loopback address. Trying next.",
                                    address, nic.getDisplayName());
                            continue;
                        }
                        if (!isPreferredAddress(address)) {
                            logger.debug("{} of {} is not a preferred IP address. Trying next.",
                                    address, nic.getDisplayName());
                            continue;
                        }
                        result = (Inet4Address) address;
                        nicDisplayName = nic.getDisplayName();
                    }
                }
            } catch (IOException ex) {
                logger.warn("Could not get a non-loopback IPv4 address:", ex);
            }

            if (result != null) {
                defaultNonLoopbackIpV4Address = result;
                logger.info("defaultNonLoopbackIpV4Address: {} (from: {})",
                        defaultNonLoopbackIpV4Address, nicDisplayName);
            } else {
                Inet4Address temp = null;
                try {
                    final InetAddress localHost = InetAddress.getLocalHost();
                    if (localHost instanceof Inet4Address) {
                        temp = (Inet4Address) localHost;
                        logger.info("defaultNonLoopbackIpV4Address: {} (from: InetAddress.getLocalHost())",
                                temp);
                    } else {
                        logger.warn("Could not get a non-loopback IPv4 address. " +
                                "defaultNonLoopbackIpV4Address is set to null.");
                    }
                } catch (UnknownHostException e) {
                    logger.warn("Unable to retrieve the localhost address. " +
                            "defaultNonLoopbackIpV4Address is set to null.", e);
                }
                defaultNonLoopbackIpV4Address = temp;
            }
        }

        private static boolean isPreferredAddress(InetAddress address) {
            final Predicate<InetAddress> predicates = Flags.preferredIpV4Addresses();
            return predicates.test(address);
        }
    }

    private static final class HasIpV6 {

        private static final boolean IPV4_PREFERRED = SystemPropertyUtil.getBoolean("java.net.preferIPv4Stack", false);
        static final boolean HAS_IPV6;

        static {
            boolean hasIpV6 = true;
            if (IPV4_PREFERRED) {
                hasIpV6 = false;
                logger.info("IPv6: disabled (java.net.preferIPv4Stack=true)");
            } else if (isLinux()) {
                final String sysfsPath = "/proc/sys/net/ipv6/conf/all/disable_ipv6";
                try {
                    final List<String> lines = Files.readAllLines(Paths.get(sysfsPath));
                    if (!lines.isEmpty() && !"0".equals(lines.get(0))) {
                        hasIpV6 = false;
                        logger.info("IPv6: disabled (from {})", sysfsPath);
                    }
                } catch (Throwable t) {
                    logger.debug("Failed to read {}", sysfsPath, t);
                }
            }

            if (hasIpV6 && hasNoIpV6NetworkInterface()) {
                hasIpV6 = false;
                logger.info("IPv6: disabled (no IPv6 network interface)");
            }

            if (hasIpV6) {
                try (ServerSocket ss = new ServerSocket()) {
                    ss.bind(new InetSocketAddress(createLocalhost6(), 0));
                } catch (IOException ignored) {
                    hasIpV6 = false;
                    logger.info("IPv6: disabled (unable to listen on ::1)");
                }
            }

            HAS_IPV6 = hasIpV6;
        }

        /**
         * io.netty.util.NetUtilInitializations#createLocalhost6()
         */
        private static Inet6Address createLocalhost6() {
            byte[] LOCALHOST6_BYTES = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1};

            Inet6Address localhost6;
            try {
                localhost6 = (Inet6Address) InetAddress.getByAddress("localhost", LOCALHOST6_BYTES);
            } catch (Exception e) {
                // We should not get here as long as the length of the address is correct.
                throw new IllegalStateException(e);
            }

            return localhost6;
        }

        /**
         * Returns {@code true} if no {@link NetworkInterface} supports {@code IPv6}, {@code false} otherwise.
         */
        private static boolean hasNoIpV6NetworkInterface() {
            try {
                final Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
                while (interfaces.hasMoreElements()) {
                    final NetworkInterface iface = interfaces.nextElement();
                    final Enumeration<InetAddress> addresses = iface.getInetAddresses();
                    while (addresses.hasMoreElements()) {
                        final InetAddress inetAddress = addresses.nextElement();
                        if (inetAddress instanceof Inet6Address && !inetAddress.isAnyLocalAddress() &&
                                !inetAddress.isLoopbackAddress() && !inetAddress.isLinkLocalAddress()) {
                            return false;
                        }
                    }
                }
            } catch (SocketException e) {
                logger.debug("Unable to detect if the machine has an IPv6 network interface", e);
                return false;
            }
            return true;
        }
    }

}
